<?php

/**
 * Pwiff - PHP SWF Library
 *
 * LICENSE
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 *
 * @package    Pwiff
 * @copyright  Copyright (c) 2010 Steven Harris
 * @license    http://www.opensource.org/licenses/bsd-license.php New BSD License
 * @version    $Id: Output.php 2 2010-02-07 04:10:51Z indigo.sunrise $
 */

/**
 * Abstract output class. Handles writing out raw datatypes as sequences
 * of bytes, which are actually written in a subclass.
 *
 * @package Pwiff
 */
abstract class Pwiff_Output
{
	/**
	 * When writing non byte-aligned data, this stores the current byte.
	 *
	 * @var int
	 */
	protected $bitBuffer = 0;
	
	/**
	 * Bit position in the current byte.
	 *
	 * @var int
	 */
	protected $bitPos = 0;
	
	/**
	 * The byte position in the output.
	 *
	 * @var int
	 */
	protected $position = 0;
	
	/**
	 * Writes a signed 8-bit integer.
	 *
	 * @param  int $byte
	 * @return Pwiff_Output
	 */
	public function writeSI8($byte)
	{
		return $this->writeUI8($byte & 0xff);
	}
	
	/**
	 * Writes an unsigned 8-bit integer.
	 * 
	 * @param  int $byte
	 * @return Pwiff_Output
	 */
	public function writeUI8($byte)
	{
		$this->byteAlign();
		$this->writeByte($byte & 0xff);
		return $this;
	}
	
	/**
	 * Writes a single character. Shortcut for writeUI8 + ord.
	 *
	 * @param  string $char
	 * @return Pwiff_Output
	 */
	public function writeChar($char)
	{
		return $this->writeUI8(ord($char));
	}
	
	/**
	 * Writes a signed 16-bit integer.
	 *
	 * @param  int $short
	 * @return Pwiff_Output
	 */
	public function writeSI16($short)
	{
		return $this->writeUI16($short);
	}
	
	/**
	 * Writes an unsigned 16-bit integer.
	 *
	 * @param  int $short
	 * @return Pwiff_Output
	 */
	public function writeUI16($short)
	{
		$this->byteAlign();
		$this->writeByte($short & 0xff);
		$this->writeByte(($short >> 8) & 0xff);
		return $this;
	}
	
	/**
	 * Writes an unsigned 24-bit integer.
	 *
	 * @param  int $data
	 * @return Pwiff_Output
	 */
	public function writeUI24($data)
	{
		// only used in one spot of the VP6 codec, but easy enough to put in
		$this->byteAlign();
		$this->writeByte($data & 0xff);
		$this->writeByte(($data >> 8) & 0xff);
		$this->writeByte(($data >> 16) & 0xff);
		return $this;
	}
	
	/**
	 * Writes a signed 32-bit integer.
	 *
	 * @param  int $int
	 * @return Pwiff_Output
	 */
	public function writeSI32($int)
	{
		return $this->writeUI32($int);
	}
	
	/**
	 * Writes an unsigned 32-bit integer.
	 *
	 * @param  int $int
	 * @return Pwiff_Output
	 */
	public function writeUI32($int)
	{
		$this->byteAlign();
		$this->writeByte($int & 0xff);
		$this->writeByte(($int >> 8) & 0xff);
		$this->writeByte(($int >> 16) & 0xff);
		$this->writeByte(($int >> 24) & 0xff);
		return $this;
	}
	
	/**
	 * @ignore
	 */
	public function writeUI64($long)
	{
		// likely awkward to implement for 32 bit systems.
		// in addition, SWF v10 doesn't even use 64 bit integers.
		// so yeah, TODO later.
	}
	
	/**
	 * Writes a 16.16 fixed point number.
	 *
	 * @param  float $fixed16
	 * @return Pwiff_Output
	 */
	public function writeFixed16($fixed16)
	{
		return $this->writeUI32(intval($fixed16 * 65536));
	}
	
	/**
	 * Writes an 8.8 fixed point number.
	 *
	 * @param  float $fixed8
	 * @return Pwiff_Output
	 */
	public function writeFixed8($fixed8)
	{
		return $this->writeUI16(intval($fixed8 * 256) & 0xffff);
	}
	
	/**
	 * Writes a half precision floating point number. Accepts a standard
	 * PHP floating point number, and manipulates the bits to remove
	 * precision and fit it into 16 bits. This method differs from the
	 * official IEEE half-precision floating point format, in that SWF
	 * uses an exponent bias of 16 while IEEE uses 15.
	 *
	 * @param  float $float
	 * @return Pwiff_Output
	 */
	public function writeFloat16($float)
	{
		$bits = Pwiff_Util_Numerics::floatToIntBits($float);
		$sign = abs(($bits & 0x80000000) >> 31);
		
		// get the 32-bit versions of the exponent and mantissa
		$exp  = ($bits & 0x7f800000) >> 23;
		$mant = $bits & 0x7fffff;
		
		// reduce 32-bit exp by Float32 bias, then add Float16 bias
		$exp = $exp - 127 + 16; // SWF is 16, IEEE is 15
		
		// restrict exponent to 5 bits, unsigned
		$exp &= 0x1f;
		if ($exp < 0) {
			$exp = 0;
		}
		
		// exp = 0 and exp = 31 are special (zero and +/-Infinity)
		if ($exp === 0 OR $exp === 0x1f) {
			$mant = 0;
		}
		else {
			// reduce mantissa to 10 bits from 23 bits
			$mant = ($mant >> 13) & 0x3ff;
		}
		
		// make short int
		$int = ($sign << 15) | ($exp << 10) | $mant;
		
		// write it
		$this->byteAlign();
		$this->writeByte($int & 0xff);
		$this->writeByte(($int >> 8) & 0xff);
		
		return $this;
	}
	
	/**
	 * Writes a single precision floating point number.
	 *
	 * @param  float $float
	 * @return Pwiff_Output
	 */
	public function writeFloat($float)
	{
		$this->byteAlign();
		$this->writeRawString(pack('f', $float));
		return $this;
	}
	
	/**
	 * Writes a double precision floating point number.
	 *
	 * @param  float $double
	 * @return Pwiff_Output
	 */
	public function writeDouble($double)
	{
		$this->byteAlign();
		$this->writeRawString(pack('d', $double));
		return $this;
	}
	
	/**
	 * Writes an encoded 32-bit unsigned integer.
	 *
	 * @param  int $int
	 * @return Pwiff_Output
	 */
	public function writeEncodedU32($int)
	{
		$this->byteAlign();
		
		if ($int === 0) {
			$this->writeByte(0);
			return $this;
		}
		
		while ($int > 0) {
			$temp = $int & 0x7f; // get 7 bits
			$int >>= 7; // shift the 7 off
			if ($int > 0) {
				// if we still have data, set the high bit
				$temp |= 0x80;
			}
			$this->writeByte($temp);
		}
		return $this;
	}
	
	/**
	 * Writes a bit sequence, given the bits in the form of an integer,
	 * and the number of bits to write.
	 *
	 * @param  int $bits
	 * @param  int $numBits
	 * @return Pwiff_Output
	 */
	public function writeBits($bits, $numBits)
	{
		while ($numBits > 0) {
			$this->bitPos++;
			
			// if the bit is set, put it in the bit buffer
			$set = $bits & (1 << ($numBits - 1));
			if ($set !== 0) {
				$this->bitBuffer |= 1 << (8 - $this->bitPos);
			}
			
			// if we reached the end of the bit, flush it to output
			if ($this->bitPos === 8) {
				$this->byteAlign();
			}
			$numBits--;
		}
		return $this;
	}
	
	/**
	 * Writes a 16.16 fixed point number as a bit sequence, given the
	 * number and the amount of bits to use.
	 * 
	 * @param  float $number
	 * @param  int   $numBits
	 * @return Pwiff_Output
	 */
	public function writeFixedBits($number, $numBits)
	{
		return $this->writeBits(intval($number * 65536), $numBits);
	}
	
	/**
	 * Writes a boolean value as a single bit.
	 * 
	 * @param  bool $bit
	 * @return Pwiff_Output
	 */
	public function writeBool($bit)
	{
		return $this->writeBits($bit ? 1 : 0, 1);
	}
	
	/**
	 * Writes raw string data to the output, with no terminating null byte.
	 *
	 * @param  string $string
	 * @return Pwiff_Output
	 */
	protected function writeRawString($string)
	{
		for ($i = 0, $l = strlen($string); $i < $l; $i++) {
			$this->writeChar($string[$i]);
		}
		return $this;
	}
	
	/**
	 * Writes a byte-aligned string to the output, followed by a null byte.
	 *
	 * @param  string $string
	 * @return Pwiff_Output
	 */
	public function writeString($string)
	{
		$this->byteAlign();
		$this->writeRawString($string);
		$this->writeByte(0);
		return $this;
	}
	
	/**
	 * Byte-aligns the stream. If the stream is currently in the middle of
	 * a byte, this will fill the remaining bits with 0 and flush the byte
	 * to output.
	 */
	public function byteAlign()
	{
		if ($this->bitPos === 0) {
			return;
		}
		$this->writeByte($this->bitBuffer & 0xff);
		$this->bitBuffer = 0;
		$this->bitPos    = 0;
	}
	
	/**
	 * Writes a single byte to output.
	 * 
	 * @param  int $byte
	 */
	abstract public function writeByte($byte);
}
